﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Linq.Expressions;

namespace Toolkit.Web.Mvc.ExtensionMethods
{
    public static class ViewDataDictionaryExtensions
    {
        /// <summary>
        /// Retrieves a view data property as the data type specified.
        /// </summary>
        /// <typeparam name="T">Property type to be returned.</typeparam>
        /// <param name="key">Associated key for the property in the view data collection.</param>
        /// <returns>Type casted view data property.</returns>
        public static T Get<T>(this ViewDataDictionary viewData, string key)
        {
            return Get<T>(viewData, key, default(T), false);
        }

        /// <summary>
        /// (Overload) Retrieves a view data property as the data type specified. If no property is found, it returns the default value given.
        /// </summary>
        /// <typeparam name="T">Property type to be returned.</typeparam>
        /// <param name="key">Associated key for the property in the view data collection.</param>
        /// <param name="defaultValue">Default value to be returned if no valid view data property is found.</param>
        /// <returns>Type casted view data property.</returns>
        public static T Get<T>(this ViewDataDictionary viewData, string key, T defaultValue)
        {
            return Get<T>(viewData, key, defaultValue, true);
        }

        /// <summary>
        /// (Overload) Retrieves a view data property as the data type specified. If no property is found, it returns the default value given.
        /// </summary>
        /// <typeparam name="T">Property type to be returned.</typeparam>
        /// <param name="key">Associated key for the property in the view data collection.</param>
        /// <param name="defaultValue">Default value to be returned if no valid view data property is found.</param>
        /// <param name="useDefaultValue">Whether or not to use the default value if no value exists.</param>
        /// <returns>Type casted view data property.</returns>
        public static T Get<T>(this ViewDataDictionary viewData, string key, T defaultValue, bool useDefaultValue)
        {
            if (viewData == null)
            {
                throw new ArgumentException();
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException();
            }

            try
            {
                return (T)viewData[key];
            }
            catch (NullReferenceException)
            {
                if (useDefaultValue) return defaultValue;
                throw new Exception(string.Format("ViewData property does not exist and no default was provided: {0}", key));
            }
            catch (InvalidCastException)
            {
                if (useDefaultValue) return defaultValue;
                throw new Exception(string.Format("ViewData property cannot be cast to the expected data type: {0}", key));
            }
        }

        /// <summary>
        /// Get the dynamically generated name attribute value for a given expression.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <typeparam name="TProperty"></typeparam>
        /// <param name="htmlHelper"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string NameFor<TModel, TProperty>(this ViewDataDictionary viewData, Expression<Func<TModel, TProperty>> expression)
        {
            return viewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
        }

        /// <summary>
        /// Get the dynamically generated id attribute value for a given expression.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <typeparam name="TProperty"></typeparam>
        /// <param name="htmlHelper"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string IdFor<TModel, TProperty>(this ViewDataDictionary viewData, Expression<Func<TModel, TProperty>> expression)
        {
            return viewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
        }

        /// <summary>
        /// Tries to retrieve the attempted value of the given model property 1st from ModelState (on POST requests). If not found there, it tries to retrieve the value
        /// from the existing ViewData (on GET requests). Also returns whether there were any errors associated with this field in the ModelState error collection.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <typeparam name="TProperty"></typeparam>
        /// <param name="viewData"></param>
        /// <param name="expression"></param>
        /// <param name="isInvalid"></param>
        /// <returns></returns>
        public static T GetAttemptedValue<T>(this ViewDataDictionary viewData, string nameAttributeValue, out bool isInvalid)
        {
            isInvalid = false;

            // Look up model state.
            ModelState modelState;
            bool useModelState = viewData.ModelState.TryGetValue(nameAttributeValue, out modelState);

            // Try to get any attempted value on this field.
            object attemptedValue = null;
            if (useModelState)
            {
                if (modelState.Value != null)
                {
                    // Try to get value from model state.
                    attemptedValue = modelState.Value.AttemptedValue.ToString();
                }

                // If there are any errors for a named field, mark field as invalid.
                if (modelState.Errors.Count > 0)
                {
                    // Inform the component that its current state is invalid.
                    isInvalid = true;
                }
            }
            else
            {
                // If value was not passed via the model state, try to lookup value from view data.
                attemptedValue = viewData.Eval(nameAttributeValue);
            }

            if (attemptedValue != null)
            {
                try
                {
                    // Handle nullable types.
                    if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    {
                        if (attemptedValue == null)
                        {
                            return default(T);
                        }
                        return (T)System.Convert.ChangeType(attemptedValue, Nullable.GetUnderlyingType(typeof(T)));
                    }
                    // Handle non-nullable types.
                    return (T)System.Convert.ChangeType(attemptedValue, typeof(T));
                }
                catch (InvalidCastException)
                {
                }
            }

            return default(T);
        }

        /// <summary>
        /// Tries to retrieve the attempted value of the given field 1st from ModelState (on POST requests). If not found there, it tries to retrieve the value
        /// from the existing model (on GET requests). Also returns whether there were any errors associated with this field in the ModelState error collection.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <typeparam name="TProperty"></typeparam>
        /// <param name="viewData"></param>
        /// <param name="expression"></param>
        /// <param name="isInvalid"></param>
        /// <returns></returns>
        public static TProperty GetAttemptedValue<TModel, TProperty>(this ViewDataDictionary<TModel> viewData, Expression<Func<TModel, TProperty>> expression, out bool isInvalid)
        {
            isInvalid = false;
            string nameAttributeValue = viewData.NameFor(expression);

            // Look up model state.
            ModelState modelState;
            bool useModelState = viewData.ModelState.TryGetValue(nameAttributeValue, out modelState);

            // Try to get any attempted value on this field.
            object attemptedValue = null;
            if (useModelState)
            {
                if (modelState.Value != null)
                {
                    // Try to get value from model state.
                    attemptedValue = modelState.Value.AttemptedValue.ToString();
                }

                // If there are any errors for a named field, mark field as invalid.
                if (modelState.Errors.Count > 0)
                {
                    // Inform the component that its current state is invalid.
                    isInvalid = true;
                }
            }
            else
            {
                // Use existing value from model.
                attemptedValue = ModelMetadata.FromLambdaExpression(expression, viewData).Model;
            }

            if (attemptedValue != null)
            {
                try
                {
                    // Handle nullable types.
                    if (typeof(TProperty).IsGenericType && typeof(TProperty).GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    {
                        if (attemptedValue == null)
                        {
                            return default(TProperty);
                        }
                        return (TProperty)System.Convert.ChangeType(attemptedValue, Nullable.GetUnderlyingType(typeof(TProperty)));
                    }
                    // Handle non-nullable types.
                    return (TProperty)System.Convert.ChangeType(attemptedValue, typeof(TProperty));
                }
                catch (InvalidCastException)
                {
                }
            }

            return default(TProperty);
        }
    }
}
